home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Games of Daze
/
Infomagic - Games of Daze (Summer 1995) (Disc 1 of 2).iso
/
x2ftp
/
msdos
/
source
/
rpg_scrl
/
programr.txt
< prev
next >
Wrap
Text File
|
1994-09-20
|
29KB
|
618 lines
PROGRAMMER'S NOTES
An Explanation of How it All Works
For Begginers
Table Of Contents
Purpose
Audience
Disclaimer
Graphics Basics (Pixels and Bytes)
Assembler - Basics
The Virtual Screen
Assembler - Drawing Tiles
The Vertical Retrace
Assembler - Showing a Virtual Screen
Interlude: Optimizing Assembler - Speed Speed Speed
Assembler - Drawing Sprites
How I Got These Pictures
Why Pascal
Why I Wrote This
Purpose
=======
This is a small tutorial for those not familiar with basic game
graphics. It shows how tiles/sprites/little pictures can be drawn
using a little bit of assembler language.
Audience
========
Many experienced programmers probably already know these things, so
this is really targeted to people who aren't exactly sure how some
of these things work.
Disclaimer
==========
Hey, I'm no genius, so some of this might be wrong and it is
most certainly inefficient. But I do know it works. The sample
code that comes with this was written in one afternoon (actually, a
programmer afternoon, which is 6pm to 2am) and was debugged in another.
I'm going to improve it eventually, just like I'll eventually add
comments (I don't feel too bad as I comment a bit more than most
programmers). Hopefully there's enough substance here to help a few
people out.
Graphics Basics (Pixels, Bytes and Mode 13h)
============================================
The video screen you're staring at is made of of thousands of tiny
pixels. A pixel is a small dot, the smallest element a program
can use. It will always be one color. But which color? That gets
set by the program running. How many choices does it have? That
depends on how many bits are assigned to a pixel. A bit is
either the number 0 or 1. On IBMs, bits are grouped in packs
of 8 called bytes (the smallest piece of information a computer
can directly affect). For video modes like text mode (your normal
black and white screen), you can store 8 pixels in a byte, and
since each pixel only gets, therefore, 1 bit, it only has the
choice of two colors (0 or 1, often white or black or on older
monitors dark green or light green). How do you change an
individual pixel if your program can only address bytes -
don't ask, you don't want to know (it involves masking, logical
operators or just plain hard coding the image). The old CGA
monitors had pixels that had 2 bits (4 colors), EGA had
4 bits (2 to the 4rth power, or 16 colors) and VGA has
8 bits (256 colors).
The IBM PCs, by nature, have planar video modes. Now that
you've heard the term, forget it - you wouldn't like it. It
involves splitting the bits of a pixel over several bytes,
so to get a color of one pixel that's 4 bits deep, you might
have to obtain the 7th bit of bytes 1,2,3 and 4 in the video
memory. Yuch. (There are advantages to this, but we'll just
ignore them here).
The PC does offer one video mode which is linear - mode 13h
(that's 13 hex, or 19 in human numbers). It creates a screen
that is 320x200 and each pixels has 8 bits, or 256 colors.
Nicely enough, 8 bits is also the size of a full byte, so we can
modify the colors without nasty masking and such.
Since mode 13h is linear, it means the pixel next to you also
happens to be the bit (or in this case, byte) in the video
memory next to you. The mode 13h screen is essentially a large
array (320x200 = 64,000) of bytes stored at the place in memory
your computer reserves for the interface to your graphics card
(this is 0A000:0000h for those curios - it's a different
address for other video modes, especially normal and
monochrome text modes which are at b800:0000 and b000:0000).
So the screen is really just
Screen = array [1..64000] of byte
If you wanted, and many have done this, for a basic way to change
the colors of pixels, you could declare an array at that offset
(Screen = array [1..64000] of byte absolute $a000) and just change
the contents directly (Screen [y*320+x]:=0; which is black).
(Note: while this is great in a sloppy language like C, in Pascal,
you cannot declare an array like the above because of range
checking considerations - the real code is Screen = array[0..0] of byte
and the PutPixel function is {$R-}Screen[y*320+x]:=color{$R+} ).
In case you noticed, to get to the proper place in the array, you
multiply the y coordinate by the length of the row (320 here) and
add the x coordinate, so the coordinates 1,2 is Screen[641].
In case your wondering, mode X, which seems popular now (for good
reason), is a planar video mode, and much more complex to program. I
won't cover it here.
Assembler - Basics
==================
The heart of all assembler routines is the movs function - move string.
It comes in many variants - movsb, movsw and movsd (for byte, word -two
bytes-, or double word -four bytes-). So in our situation, this
will draw 1 pixel, 2 pixels or four pixels in one fell swoop.
How does movs know where to get the data to copy and where to copy
to? It uses certain built-in assembler variables - es,di,ds and si.
Each of these is a word (2 bytes or 16 bits). The place to copy from
is ds:si and the place to copy to is es:di (the : means put them
together - it's segment offset format, a dos segmented memory
model that plenty of people complain about).
And how do figure out what numbers to put in there? Well, in
assembler, the mov command is like the pascal := or the C =
but that only helps so much because es and ds (extra segment
and data segment) are wierd - you can't just copy any number
into these places, they can only have values moved into them
with mov from an assembler register (seen below). So you have
another option - les and lds (load es and ds respectively).
You can use these with your variables. So say you have
an picture stored, byte for byte, in something like
Picture = array [1..32,1..32] of byte; Fred : Picture.
You would say copy from my picture by lds si,Picture
which will get the segment and offset of wherever it
is the program places your picture in memory and put it
in ds:si.
Assembler offers 4 basic registers - ax,bx,cx, and dx. Each is
a word long. Sometimes they have special purposes - for example,
cx is used by many assembler commands as a counter. You can load
anything into these with the mov command.
So you wanna draw something, right? Let's copy one pixel from
the top left corner of our picture (in format array[1..32,1..32] of
byte) into the top left corner of the screen. We'd do:
mov ax,a000h ; where the screen starts
mov es,ax ; load it into es
mov di,0 ; set the rest to 0 so it's a000:0000
lds si,MyPicture ; get the coordinate of our picture
movsb ; and copy our pixel
Note: the movs command automatically increments the source and
destination pointers. So after this call, es:di, which was
a000:0000 is now a000:0001. A call to movsw would have added two
and a call to movsd (for 386s only) would have added four
(a000:0002 and a000:0004 respectively). So if you called movsb
again it would copy the next pixel in your picture to the next spot on
the screen.
One more thing you should know - many compilers have limitations on
the kind of assembler you can use inside it. For example, my copy of
Turbo Pascal and Borland C only allow me to use 286 instructions, not
386 instructions. It's 386 instructions that allow things like movsd and
eax,,ebx,etc (32-bit, or double-word, registers). And my compiler
doesn't even use 286 instructions by default - I had to go to copiler
options to even get that (it starts with 8086 instructions, the next
step down).
The Virtual Screen
==================
It is often beneficial to have a "scratch" screen. In arcade games
(which are also tile-based by the way) they typically draw in layers -
1)Draw Background 2)Draw monsters and heroes and objects 3)Draw things
that obscure the hero (like walking behind a cliff or tree and covering
the hero). You probably don't want to show all that as you perform it.
It's easier to just take your time drawing on an offscreen buffer and
copying it when it's done.
So what's an offscreen buffer? Just like the screen, it's an array. And
how big is it? Well, if you use it to draw your whole screen, it's going
to be [1..64000] of byte in mode 13h. In the example program, only a
small portion (the travel map) is drawn, so make it as big as you need
to.
Assembler - Drawing Tiles
=========================
Here's where you learn how to draw a picture in an offscreen buffer. It
assumes you have a picture. I use a tile type that's 32 pixels x 32.
It's stored, pixel by pixel, top left to bottom right, in an
array[1..32,1..32] of byte. The screen here is 9 tiles by 7 tiles (you
can figure the pixels yourself - 9*32 x 7*32). The procedure is as
follows:
Procedure PlaceTileInBuffer (PixelX,PixelY:word; var Pic:icon32); assembler;
const
WordLength = TileWidth div 2;
Asm
(* figure pixel offset in buffer *)
mov ax,BufWidth (* get length of buffer - 9*32 *)
mul PixelY (* drop down to the line you want *)
(* This automatically multiples the *)
(* Y-val by ax, which is the X-val *)
add ax,PixelX (* gives (Y*width)+x *)
(* preserve data segment pointer *)
(* Other things used this before you got it and will use *)
(* it again when you're done with it, so save the value *)
(* by moving it someplace we won't hurt it or alternately *)
(* you could save it with push dx and restore with pop dx *)
mov dx,ds
(* Copy to where? *)
les di,buffer (* buffer is your offscreen buffer *)
mov di,ax (* move to the pixel in the buffer *)
(* you want to start at *)
(* Copy from where? *)
lds si,Pic (* load your picture *)
(* Copy Data *)
mov bx,TileHeight (* here, 32 pixels *)
@@CopyRowLoop:
mov cx,WordLength (* how many words long is the row? *)
push di (* save offset *)
rep movsw (* copy cx words to the buffer *)
pop di (* restore offset *)
add di,BufWidth (* go to next line *)
dec bx (* finished that row already *)
jnz @@CopyRowLoop (* if there are any more rows in bx *)
(* go ahead and do this again *)
(* OK, all done, so quit *)
mov ds,dx (* restore data segment pointer *)
End;
Ok, so what's it do? Basically, the code is something like
Figure where in the buffer to draw this
Specify where you're copying to
Specify where you're copying from
For i:= 1 to Number Of Rows do
Copy Row to Buffer
We used WordLength (16 words, since it was 32 bytes) so we could copy
using movsw, which is supposed to be quicker than movsb. If this had
been 386-optimized code, we would have done mov cx,8 rep movsd. And if
you haven't figured it out, rep is repeat, which says do the following
commands cx number of times (after it's done cx will be equal to 0).
Now why did we copy row by row? It's because we only want to copy to a
certain place (we're only copying one tile to a whole screen full
of tiles). Imagine
-------------------------------
| | | | | | |
-------------------------------
| | | | | | |
-------------------------------
| | |000 | | | |
-------------------------------
| | | | | | |
-------------------------------
| | | | | | |
-------------------------------
Ok, it's not drawn to scale, but you get the point - the lines are
your virtual screen and the 000 is the picture you just copied. You
figure the x,y coords here (if the array is linear, like [1..64000] then
the first 320 bytes are the first row, so the beggining of the second
row is 321 and the beggining of the third row is 641, and the third
pixel on the second row is 643, or in other words, (Y*BufferWidth)+X).
So if es:di is pointing to, say, 0000:0010, which is the beggining of
that tile, then you'd save that number (push di), copy 32 pixels (which
means es:di is now 0042), restore the beggining of the picture (pop di),
and move to the next line (add BufferWidth, or 320, making it 0330). If
we didn't do this, we'd end up copying a tile that's 1 pixel high and
really really long (32*32, which actually would wrap for about 3 lines).
And what, pray tell, is meant by
@@CopyRowLoop:
dec bx
jnz @@CopyRowLoop
Well, we're going to use bx to store the number of rows (32). BX is used
by the jump commands (jmp,jnz,jz,je,jne,etc). Jnz is jump if not zero,
and dec bx means decrease (subtract 1). The @@: is pascalish for local
label, which means jump to here (yes, it's a goto statement).
So we say bx=32, copy a row, subtract one from bx, if bx does not equal
0 (if we have any rows left) go back and do it again. Alternately,
rather than saving the starting pixel, we could have just added
BufWidth-TileWidth. Whichever you prefer.
We could also have used a loop statement (loop @@CopyEtc:) but it checks
cx, which we were using for something else (the number of bytes to copy
in rep movsw).
So is this beggining to make sense?
Assembler - Showing a Virtual Screen
====================================
So how do you show a virtual screen?
Well, if it depends on the size of the buffer. If the buffer is not the
same size as the screen (which ours isn't) you pretend the buffer is
just a big tile and copy it to the screen. Remember, you load the screen
by doing
mov ax,0a000h
mov es,ax
xor di,di ; this says di=0, which is faster than mov di,0
And if it's the same size as the screen? Even better. Then we don't have
to do any of this complicated adjusting for the next row stuff. It's
just:
push ds (* save the data segment *)
lds si,Buffer; (* load scratch page *)
mov ax,Screen_Offset; (* load screen coords *)
mov es,ax (* es = 0a000h *)
xor di,di (* di = 0 *)
mov cx,32000 (* copy 32,000 words / 64,000 bytes *)
rep movsw (* and do the copy *)
pop ds (* restore the data segment *)
The Vertical Retrace
====================
So you almost have what you need - except that matter about the vertical
retrace. The way a monitor works, a small beam inside the monitor shoots
at the screen (the pixels/dot pitch, which are small pieces of phosphor)
going from left to right, top to bottom (except on interlaced monitors
which skips every other line and comes back for them later).
When the beam strikes the phosphorus, it sets it to the color it has in
it's memory. So what happens when you get unlucky and start drawing when
the screen is halfway through the vertical retrace (that is, halfway
through redrawing)? You draw only half of your picture until the next
pass through. Which looks dorky. So what you need to do is wait until
the vertical retrace is done, then copy your data to the screen -
quickly. If you monitor refreshes 70 times a second, you have 1/70th of
a second to copy your data in. That's why it's nice for the copy to
screen routine to be fast (and why we use virtual screens to put things
together before we finally blit them to the screen).
And how do we do it?
(* Wait for Vertical Retrace *)
cli
mov dx,3DAh
@@label1:
in al,dx
and al,08h
jnz @@label1
@@label2:
in al,dx
and al,08h
jz @@label2
sti
(* End Check for Retrace *)
How's it work? You don't want to know - trust me. It turns off certain
interrupt handlers then makes calls to the VGA ports (3DAh) and tests
the results. All you need to know is that it works. This code is
available from many ftp sites and numerous books (like the Programmer's
Guide to the EGA/VGA, very good if you're the type who likes reading
dictionaries).
Interlude: Optimizing Assembler - Speed Speed Speed
===================================================
Ok, a few assembler rules. You want as few lines as possible, although
it's important to keep in mind that some instructions are slower than
others. For example xor ax,ax is slower than mov ax,0 (don't know what
and, or and xor do? They're logical operators. Given two conditions, say
It Is Raining and My Dog Is White, and returns true (1) if both are
true, or returns true if either is true, and xor returns true if only
one is true. So for us, if you ANDed the number 101 and 011, the answer is
001 - only the last digit is true (1) in each number. If it had been
ORed, the number would have been 111, and had it been XORed, the answer
would have been 110. Any number XORed to itself is always all false,
that is 00000000).
So where can we save some room? Movsw is faster than movsb, and movsd is
faster than movsw. That'll be important in the next section, as when we
copy sprites (anything with a transparent background) we can only use
movsb.
You can also try unrolling your loops. If you're copying two rows,
mov bx,TileHeight
@@CopyRowLoop:
mov cx,WordLength
push di
rep movsw
pop di
add di,BufWidth
dec bx
jnz @@CopyRowLoop
is slower than
(* copy one row *)
mov cx,WordLength
push di
rep movsw
pop di
add di,BufWidth
(* copy second row *)
mov cx,WordLength
push di
rep movsw
pop di
add di,BufWidth
That's because jump and loop statements (like jnz) are slow. In the
latter example you're doing the same basic work without three of the
lines (mov bx,TileHeight dec bx jnz @@CopyRowLoop), which means three
less lines to execute.
So what's the catch (yes, there is one) - this isn't very portable,
which is a standard rule of assembler : specific routines are faster
than generic ones. If you know your icons are 32x32 and your buffer is
320x200, you can code
mov cx,16
rep movsw
add di,288
(repeat 31 times more)
The problem is, short of long, ugly code, is that this routine is no
good for icons 16x16. For that, you'd write another routine (same code
but different magic numbers - mov cx,8 add di,304). But if you're
willing to do that, you can speed things up a bit. But be forewarned -
if you buy or use a library from someone else (say the BGI functions
that come with Borland products), chances are their blitting functions
(functions to copy rectangular areas like sprites) are slower because of
all the extra overhead. And lord forbid if those functions serve more
than one video mode (like EGA and VGA).
There are other places where code can be optimized, where certain
statements are faster than others (on certain computers) but I don't
know enough about assembler, frankly, to tell you what they are.
Assembler - Drawing Sprites
===========================
Sprites are like tiles, except they can do something nifty - they don't
look like squares. Why? Because in our code, we specify a color that we
will not draw. Examine the following:
const
WordLength = TileWidth div 2;
Asm
(* figure pixel offset in buffer *)
mov ax,BufWidth (* we've seen this before, right? *)
mul PixelY
add ax,PixelX (* gives (Y*width)+x *)
(* preserve data segment pointer *)
push es (* I'm not sure es is so important *)
push ds (* but this sure is *)
(* Copy to where? *)
les di,buffer (* point to our virtual screen *)
mov di,ax
(* Copy from where? *)
lds si,Pic (* point to our sprite *)
(* Copy Data - Don't draw black (color 0) pixels *)
mov bx,TileHeight
@@CopyRowLoop:
mov cx,TileWidth (* how many words long is the row? *)
push di (* save offset *)
@@PutPixel:
mov ax,[ds:si] (* retrieve value in ds:si *)
cmp ax,0 (* is this a black (#0) pixel? *)
je @@SkipPixel (* if so, skip it (goto SkipPixel *)
movsb (* copy cx words to the buffer *)
loop @@PutPixel (* keep looping until cx=0 *)
(* Move to Next Row *)
@@EndOfRow:
pop di (* restore offset *)
add di,BufWidth (* go to next line *)
dec bx (* finished that row already *)
jnz @@CopyRowLoop (* if there are any more rows in bx *)
(* go ahead and do this again *)
jmp @@Done
@@SkipPixel:
inc di (* if we don't do movsb then we must *)
inc si (* manually increase si and di *)
dec cx (* and decrease the counter *)
cmp cx,0 (* are we at the end of the row? *)
je @@EndOfRow (* if so, go to end of row line *)
jmp @@PutPixel (* otherwise, do the next pixel *)
(* OK, all done, so quit *)
@@Done:
pop ds (* restore data segment pointer *)
pop es
End;
And what does it mean? Basically, it's
for y:=1 to number of rows do
for x:=1 to number of pixels in a row do
if the pixel is not black
movsb
else
manually update the pointers
(ie., move over to next pixel)
In here
@@PutPixel:
mov ax,[ds:si] (* retrieve value in ds:si *)
cmp ax,0 (* is this a black (#0) pixel? *)
je @@SkipPixel (* if so, skip it (goto SkipPixel *)
movsb (* copy cx words to the buffer *)
loop @@PutPixel (* keep looping until cx=0 *)
we're reading the value pointed to by ds:si (ds:si is a number like
0000:0123 while [ds:si] is a byte like the number 4 or 256) and then
comparing that to 0 (the color we don't want to draw). If it's not the
unwanted color, then do the old familiar movsb (if we did movsw, we'd
have copied the next pixel too, whether it was the unwanted color or
not). And then we go back to the PutPixel loop until we've finished
copying all the data for that row (ie., when cx=0).
And if ax does hold the color we don't want?
@@SkipPixel:
inc di (* if we don't do movsb then we must *)
inc si (* manually increase si and di *)
dec cx (* and decrease the counter *)
cmp cx,0 (* are we at the end of the row? *)
je @@EndOfRow (* if so, go to end of row line *)
jmp @@PutPixel (* otherwise, do the next pixel *)
Movsb moves over to the next pixel for us and decreases the counter
(cx). If we don't use movsb (we don't draw the pixel) then we have to
manually update these. We also need to check cx to see if we just drew
(or in this case skipped) the last pixel in that row. If not, we go to
the next pixel, if so, we do the end of row routines like normal (move
to the next line and reset the counter).
Needless to say, given all the tests and jumping around we do, this is
not the fastest routine there is.
How I Got These Pictures
========================
Well, that's about all there is to it to the tiny tutorial on tile-based
graphics. I hope my spelling held out so far (it normally doesn't and
I'm not going to spend the time editing this, so forgive any nonsense
sentences).
But how did I get those pictures? The old question of sprite editors and
the like. I used a really convulted way - draw the pictures in Deluxe
Animator (my drawing program of choice) and then writing a small program
that displays the reulting pcx file created by DA and moving a 32x32
rectangle down to the picture I want. I then press enter and it saves
that 32x32 region to disk, which I promptly rename. I will eventually
write another sprite editor (I've written several already, mostly for
ega and text font editors) but for now this works for me. Sorry this
doesn't help to many of you out.
The icons are easy - just layer colors next to each other (like the
water which is light blue with a dark blue undercoating which helps set
it off). I've seen columns done in arcade games with a solid white line
in the middle and just small, darker lines on either side (sounds silly
but it actually looks ok). The grass was a solid green background with
DA's spray paint tool used to put light green and black on top of it.
I hear some people make 256-color sprite editors, though I never saw one
I liked. Many people use Autodesk Animator (which I don't own
regretably but hear is nice). Others (like Sierra) just paint pictures
and scan them in. If you get a drawing tool, what I do to cheat is to
place a piece of see-through paper on the screen with a drawing I did by
hand and then try to trace that with the mouse on the screen.
But writing a tile-editor is nice, because eventually you'll have to
write a map editor (if no one's seen one, I have one hardcoded for a
game I wrote once I could show around, but it let's you draw maps by
picking tiles the way you normally pick colors and drawing those
around with PutTile commands like those we did here).
Why Pascal
==========
This demo was written in pascal because I didn't want to waste time with
stupid errors that C seems to like so much. The code is just as fast or
faster in turbo pascal than C, and the executable's smaller because of
the way TPUs are linked in versus C libraries.
But the truth is either is pretty much the same. They use almost the
same commands and nothing in this demo is so wierd that couldn't be
translated, line for line, to C. For example, the hard stuff (assembler)
was Asm stuff... End; in pascal and is asm { stuff } in C. Not so bad.
Given that I'll probably rewrite parts of this in C++ (or at least a
game using many of these constructs) I'm glad it'll port nicely. Truth
is, if you know a language and want a quick game, write with what you
know. Last I heard, the fastest compiler was a basic compiler anyway
(made by some European or Australian company - heard the documentation is
pretty bad though). Just do what feels good - the fast parts are in
assembler and the rest doesn't hurt your performance no matter what
langauge you use. Cobol with these routines would make a nice fast game.
Why I Wrote This
================
Three years ago (Spring 1991) my roommate, seeing I was depressed with
my major (journalism), talked me into using a computer, signing up for
beggining programming. 3 years wasn't that long ago, so I remember quite
clearly feeling like maybe I was a bright guy and still had no clue how
to do what I wanted. Computers frustrated me at every turn.
Writing computer games isn't a matter of intelligence, it's a matter of
experience. If you're a bright person, there's no reason why you
shouldn't have some help learning the basics, and no one should make you
feel dumb (as I often did) for not knowing.
I wish I had something like this to help me rather than all those
(expensive) books I ended up buying that only briefly touched on what I
wanted to know. I am indebted to people like Josh Jensen, Themie, Vic
Putz, Dr Cat and others who helped me out when I was completely lost. I
certainly wouldn't know anything if it hadn't been for other people's
help. Hopefully I can help pay them back by passing on what meager
amount I know.
And by the way, I never did changes majors to computer science. It took
me 5 semesters to pass calculus, at which point I figured I'd cop out
and get a business degree. And now I wear a tie. Friends don't let
friends wear ties.
- baylor